package com.sq.util.encrypt.ecdsa;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.asn1.ASN1InputStream;
import org.spongycastle.asn1.ASN1Integer;
import org.spongycastle.asn1.ASN1Primitive;
import org.spongycastle.asn1.DERSequenceGenerator;
import org.spongycastle.asn1.DLSequence;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.crypto.ec.CustomNamedCurves;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.crypto.signers.ECDSASigner;
import org.spongycastle.crypto.signers.HMacDSAKCalculator;
import org.spongycastle.math.ec.FixedPointUtil;

import com.sq.util.ByteUtil;
import com.sq.util.exception.CommonException;

/**
 * @author sq
 *
 */
public class ECDSASignature {

    private static final Logger log = LoggerFactory.getLogger(ECDSASignature.class);

    public static final BigInteger HALF_CURVE_ORDER;

    /** The parameters of the secp256k1 curve that Bitcoin uses. */
    protected static final ECDomainParameters CURVE;

    // The parameters of the secp256k1 curve that Bitcoin uses.
    private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");

    static {
        // Tell Bouncy Castle to precompute data that's needed during secp256k1
        // calculations. Increasing the width
        // number makes calculations faster, but at a cost of extra memory usage
        // and with decreasing returns. 12 was
        // picked after consulting with the BC team.
        FixedPointUtil.precompute(CURVE_PARAMS.getG(), 12);
        CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(),
                CURVE_PARAMS.getH());
        HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
    }

    protected ECDSASignature() {

    }

    protected static byte[] sign(byte[] input, String hexPrivateKey) {
        return sign(input, ByteUtil.toBytes(hexPrivateKey));
    }

    protected static byte[] sign(byte[] input, byte[] privateKey) {
        return sign(input, new BigInteger(privateKey)).encodeToDER();
    }

    protected static boolean verify(byte[] data, byte[] sign, byte[] publicKey) {
        if (Secp256k1Context.isEnabled()) {
            try {
                return NativeSecp256k1.verify(data, sign, publicKey);
            } catch (CommonException e) {
                log.error("Caught AssertFailException inside secp256k1", e);
                return false;
            }
        }
        return verify(data, ECDSASignature.decodeFromDER(sign), publicKey);
    }

    private static boolean verify(byte[] data, ECDSASignature signature, byte[] pub) {
        if (Secp256k1Context.isEnabled()) {
            try {
                return NativeSecp256k1.verify(data, signature.encodeToDER(), pub);
            } catch (CommonException e) {
                log.error("Caught AssertFailException inside secp256k1", e);
                return false;
            }
        }

        ECDSASigner signer = new ECDSASigner();
        ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE);
        signer.init(false, params);
        try {
            return signer.verifySignature(data, signature.r, signature.s);
        } catch (NullPointerException e) {
            // Bouncy Castle contains a bug that can cause NPEs given specially
            // crafted signatures. Those signatures
            // are inherently invalid/attack sigs so we just fail them here
            // rather than crash the thread.
            log.error("Caught NPE inside bouncy castle", e);
            return false;
        }
    }

    private static ECDSASignature sign(byte[] input, BigInteger privateKeyForSigning) {
        if (Secp256k1Context.isEnabled()) {
            byte[] signature = NativeSecp256k1.sign(input, bigIntegerToBytes(privateKeyForSigning, 32));
            return ECDSASignature.decodeFromDER(signature);
        }
        ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
        ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE);
        signer.init(true, privKey);
        BigInteger[] components = signer.generateSignature(input);
        return new ECDSASignature(components[0], components[1]).toCanonicalised();
    }

    /** The two components of the signature. */
    private BigInteger r, s;

    /**
     * Constructs a signature with the given components. Does NOT automatically
     * canonicalise the signature.
     */
    private ECDSASignature(BigInteger r, BigInteger s) {
        this.r = r;
        this.s = s;
    }

    /**
     * Returns true if the S component is "low", that means it is below
     * {@link ECKey#HALF_CURVE_ORDER}. See <a href=
     * "https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures">
     * BIP62</a>.
     */
    private boolean isCanonical() {
        return s.compareTo(HALF_CURVE_ORDER) <= 0;
    }

    /**
     * Will automatically adjust the S component to be less than or equal to
     * half the curve order, if necessary. This is required because for every
     * signature (r,s) the signature (r, -s (mod N)) is a valid signature of the
     * same message. However, we dislike the ability to modify the bits of a
     * Bitcoin transaction after it's been signed, as that violates various
     * assumed invariants. Thus in future only one of those forms will be
     * considered legal and the other will be banned.
     */
    private ECDSASignature toCanonicalised() {
        if (!isCanonical()) {
            // The order of the curve is the number of valid points that exist
            // on that curve. If S is in the upper
            // half of the number of valid points, then bring it back to the
            // lower half. Otherwise, imagine that
            // N = 10
            // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid
            // solutions.
            // 10 - 8 == 2, giving us always the latter solution, which is
            // canonical.
            return new ECDSASignature(r, CURVE.getN().subtract(s));
        } else {
            return this;
        }
    }

    /**
     * DER is an international standard for serializing data structures which is
     * widely used in cryptography. It's somewhat like protocol buffers but less
     * convenient. This method returns a standard DER encoding of the signature,
     * as recognized by OpenSSL and other libraries.
     */
    private byte[] encodeToDER() {
        try {
            return derByteStream().toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e); // Cannot happen.
        }
    }

    private static ECDSASignature decodeFromDER(byte[] bytes) throws IllegalArgumentException {
        ASN1InputStream decoder = null;
        try {
            decoder = new ASN1InputStream(bytes);
            final ASN1Primitive seqObj = decoder.readObject();
            if (seqObj == null)
                throw new IllegalArgumentException("Reached past end of ASN.1 stream.");
            if (!(seqObj instanceof DLSequence))
                throw new IllegalArgumentException("Read unexpected class: " + seqObj.getClass().getName());
            final DLSequence seq = (DLSequence) seqObj;
            ASN1Integer r, s;
            try {
                r = (ASN1Integer) seq.getObjectAt(0);
                s = (ASN1Integer) seq.getObjectAt(1);
            } catch (ClassCastException e) {
                throw new IllegalArgumentException(e);
            }
            // OpenSSL deviates from the DER spec by interpreting these values
            // as unsigned, though they should not be
            // Thus, we always use the positive versions. See:
            // http://r6.ca/blog/20111119T211504Z.html
            return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        } finally {
            if (decoder != null)
                try {
                    decoder.close();
                } catch (IOException x) {
                }
        }
    }

    private ByteArrayOutputStream derByteStream() throws IOException {
        // Usually 70-72 bytes.
        ByteArrayOutputStream bos = new ByteArrayOutputStream(72);
        DERSequenceGenerator seq = new DERSequenceGenerator(bos);
        seq.addObject(new ASN1Integer(r));
        seq.addObject(new ASN1Integer(s));
        seq.close();
        return bos;
    }

    private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
        CommonException.isTrue(b.signum() >= 0, "b must be positive or zero");
        CommonException.isTrue(numBytes > 0, "numBytes must be positive");
        byte[] src = b.toByteArray();
        byte[] dest = new byte[numBytes];
        boolean isFirstByteOnlyForSign = src[0] == 0;
        int length = isFirstByteOnlyForSign ? src.length - 1 : src.length;
        CommonException.isTrue(length <= numBytes, "The given number does not fit in " + numBytes);
        int srcPos = isFirstByteOnlyForSign ? 1 : 0;
        int destPos = numBytes - length;
        System.arraycopy(src, srcPos, dest, destPos, length);
        return dest;
    }

}